package ar.edu.itba.pod.legajo47573;


import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

import ar.edu.itba.balance.api.AgentsBalancer;
import ar.edu.itba.balance.api.AgentsTransfer;
import ar.edu.itba.event.EventInformation;
import ar.edu.itba.event.RemoteEventDispatcher;
import ar.edu.itba.node.Node;
import ar.edu.itba.node.NodeInformation;
import ar.edu.itba.node.api.ClusterAdministration;
import ar.edu.itba.node.api.StatisticReports;
import ar.edu.itba.pod.agent.runner.Simulation;
import ar.edu.itba.pod.time.TimeMapper;

public class MyNode implements Runnable{

//	private static final int PULL_SLEEP = 1000;
	private static final int NEWEVENTS_SLEEP = 2000;
	private static final int EMPTYPROCQUEUE_SLEEP = 10000;
	private NodeInformation info;
	private ClusterAdministration cluster;
	private Registry registry;
	private EventDispatcherImpl eventDispatcher;
	private RemoteEventDispatcher remoteDispatcher;
	private AgentsBalancer balancer;
	private AgentsTransfer transfer;
	private Simulation simulation;
	private NodeInformation coordinator;
	private StatisticReports statistics;

	public MyNode(NodeInformation info, TimeMapper timeMapper){
		this.info = info;
		try {
			this.registry = LocateRegistry.createRegistry(this.info.port());
			this.cluster = new ClusterAdministrationImpl(this);
			this.remoteDispatcher = new RemoteEventDispatcherImpl(this);
			this.balancer = new AgentsBalancerImpl(this);
			this.transfer = new AgentsTransferImpl(this);
			this.statistics = new StatisticReportsImpl(this);
		} catch (RemoteException e) {
			System.out.println("Error mientras se creaba el nodo");
			e.printStackTrace();
		}
		this.simulation = new MyLocalSimulation(this, timeMapper);
		eventDispatcher = ((EventDispatcherImpl)((MyLocalSimulation)simulation).dispatcher());
	}	

	public StatisticReports getStatistics(){
		return this.statistics;
	}
	
	public AgentsTransfer getTransfer(){
		return transfer;
	}

	public EventDispatcherImpl getDispatcher(){
		return eventDispatcher;
	}

	public AgentsBalancer getBalancer(){
		return balancer;
	}

	public void setCoordinator(NodeInformation coordinator){
		this.coordinator = coordinator;
	}

	public NodeInformation getCoordinator(){
		return coordinator;
	}

	public Registry getRegistry(){
		return this.registry;
	}

	public Simulation getSimulation(){
		return simulation;
	}

	public RemoteEventDispatcher getRemoteDispatcher(){
		return remoteDispatcher;
	}

	public ClusterAdministration getCluster(){
		return cluster;
	}

	public NodeInformation getInfo(){
		return info;
	}

	private void deleteProcessedEvents() throws InterruptedException{
		while(true){
			Thread.sleep(EMPTYPROCQUEUE_SLEEP);
			((RemoteEventDispatcherImpl)this.remoteDispatcher).deleteProcessedEvents();
		}
	}

	private void newEventsFor() throws InterruptedException{
		while(true){
			Thread.sleep(NEWEVENTS_SLEEP);
			Set<NodeInformation> nodes;
			try {
				nodes = new HashSet<NodeInformation>(this.cluster.connectedNodes());

				if(nodes.size() > 0){
					Random rand = new Random(13);
					int index = rand.nextInt(nodes.size());
					NodeInformation selectedNode = (NodeInformation) nodes.toArray()[index];

					RemoteEventDispatcher stub = (RemoteEventDispatcher)Registries.getRemoteObject(this, selectedNode, Node.DISTRIBUTED_EVENT_DISPATCHER);
					Set<EventInformation> newEvents = null;
					try {
						newEvents = stub.newEventsFor(getInfo());
					} catch (Exception e1) {
						try {
							this.cluster.disconnectFromGroup(selectedNode);
						} catch (Exception e) {
							System.out.println("Error al desconectar el nodo");
							e.printStackTrace();
						}
					}

					//System.out.println("NEW EVENTS " + newEvents);
					if(newEvents != null){
					for(EventInformation it : newEvents){
						try {
							getRemoteDispatcher().publish(it);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					}
				} 
			} catch (RemoteException e2) {
				e2.printStackTrace();
			}
		}
	}

	private void queuePull() throws InterruptedException{
		float probability = 1;
		Random rand = new Random(7);
		float random;
		while(true){
		//	Thread.sleep(PULL_SLEEP);
			random = rand.nextFloat();
			//Queda preguntando por eventos nuevos
			//synchronized(MyNode.class){
			if(!((RemoteEventDispatcherImpl)remoteDispatcher).noPendingEvents()){
				EventInformation eventInfo = ((RemoteEventDispatcherImpl) remoteDispatcher).getHead();
				probability = 1;
				try {
					try{
						eventDispatcher.localPublish(eventInfo.source(),eventInfo.event());
					} catch(NullPointerException e){
						
					}
				} catch (InterruptedException e) {
					System.out.println("Error al publicar un evento");
					e.printStackTrace();
				}

				Set<NodeInformation> nodes = null;
				try {
					nodes = new HashSet<NodeInformation>(cluster.connectedNodes());
				} catch (RemoteException e2) {
					e2.printStackTrace();
				}
				/*Me publico a mi*/
				//ProcessedEvent processedEvent = new ProcessedEvent(eventInfo, System.nanoTime());
				((RemoteEventDispatcherImpl) remoteDispatcher).addProcessedEvents(eventInfo);
				for(NodeInformation it : nodes){
					if(random <= probability){
						RemoteEventDispatcher stub = (RemoteEventDispatcher)Registries.getRemoteObject(this, it, Node.DISTRIBUTED_EVENT_DISPATCHER);
						try {
							/*Publico remotamente a todos*/
							if(!stub.publish(eventInfo)){
								probability = (float) (probability - 0.1);
							}
							else{
								//System.out.println("PUBLICO " + eventInfo);
							}
						} catch (Exception e) {
							try {
								this.cluster.disconnectFromGroup(it);
							} catch (Exception e1) {
								System.out.println("Error al desconectar el nodo");
								e1.printStackTrace();
							}
						}// catch (InterruptedException e) {
//							e.printStackTrace();
//						}
					}		
				}
			}
		//	}
		}
	}

	Thread newEventsFor = new Thread(){
		public void run(){
			try {
				newEventsFor();
			} catch (InterruptedException e) {
				this.interrupt();
			}
		}
	};

	Thread queuePull = new Thread(){
		public void run(){
			try {
				queuePull();
			} catch (InterruptedException e) {
				this.interrupt();
			}
		}
	};

	Thread deleteProcessedEvents = new Thread(){
		public void run(){
			try {
				deleteProcessedEvents();
			} catch (InterruptedException e) {
				this.interrupt();
			}
		}
	};

	@Override
	public void run() {
		queuePull.start();
		newEventsFor.start();
		deleteProcessedEvents.start();
	}

	public void interrupt(){
		queuePull.interrupt();
		newEventsFor.interrupt();
		deleteProcessedEvents.interrupt();
	}
}
